/*
 * Copyright 2018-2020 by jason. All Rights Reserved.
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation for any purpose and without fee is hereby granted,
 * provided that the above copyright notice appear in all copies and that
 * both that copyright notice and this permission notice appear in
 * supporting documentation, and that the name of Vinay Sajip
 * not be used in advertising or publicity pertaining to distribution
 * of the software without specific, written prior permission.
 * VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
 * ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
 * VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
 * ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
 * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

package com.tlkj.common.curd.interceptor;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson2.JSONObject;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageInfo;
import com.tlkj.common.curd.constant.enums.ReturnType;
import com.tlkj.common.curd.entity.ApiParams;
import com.tlkj.common.curd.exception.OpenApiException;
import com.tlkj.common.curd.model.AjaxResult;
import com.tlkj.common.curd.service.ApiService;
import com.tlkj.common.curd.util.PageUtil;
import com.tlkj.common.curd.vo.ApiVo;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.DependsOn;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.*;

/**
 * @author flyong86
 * @since 2024年6月20日
 */
@Component
public class ApiInterfaceInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(ApiInterfaceInterceptor.class);

    private final SqlSessionFactory sessionFactory;

    @Resource
    private ApiService apiService;

    public String urlPrefix = "/common/curd";

    public ApiInterfaceInterceptor(SqlSessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            ApiVo api = getInterfaceByPath(request, urlPrefix);

            Map<String, Object> params = getRequestParams(request, api.getRequestParams());

            // 是否分页
            boolean pageable = Boolean.parseBoolean(request.getParameter("pageable"));
            Page<?> page = pageable ? getPage(request) : null;

            // 执行语句
            Object result = executeDynamicScript(api, params, pageable, page);

            //设置跨域
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json");
            response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "*");
            response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "*");
            response.getWriter().write(JSON.toJSONString(AjaxResult.success(result), SerializerFeature.WriteMapNullValue));

        } catch (Exception e) {
            log.error("执行SQL异常!", e);
        }
        return false;
    }

    /**
     * 获取接口名称
     */
    private ApiVo getInterfaceByPath(HttpServletRequest request, String urlPrefix) throws Exception {
        String path = request.getRequestURI();

        urlPrefix = request.getContextPath() + urlPrefix;
        // 获取虚拟路径
        String virtualPath = path.replace(urlPrefix, "");
        ApiVo openInterface = apiService.findByPath(virtualPath, null);
        if (null == openInterface) {
            throw new OpenApiException("该接口不存在");
        } else if (!openInterface.getEnable()) {
            throw new OpenApiException("该接口未发布，无法进行使用!");
        }
        return openInterface;
    }

    /**
     * 获取请求参数.
     * @param request 请求对象
     * @param apiParams 接口参数定义
     * @return 请求参数
     */
    private Map<String, Object> getRequestParams(HttpServletRequest request, List<ApiParams> apiParams) throws IOException {

        if (null == apiParams || apiParams.isEmpty()) {
            return null;
        }

        Map<String, Object> paramsMap = new HashMap<>(apiParams.size());
        for (ApiParams apiParam : apiParams) {
            Object val = request.getParameter(apiParam.getName());
            if (val == null && StrUtil.isNotEmpty(apiParam.getDefaultVal())) {
                val = apiParam.getDefaultVal();
            }
            paramsMap.put(apiParam.getName(), val);
        }

        // 获取Body里面的参数
        if (request.getMethod().equals(HttpMethod.POST.name())) {
            StringBuilder sb = new StringBuilder();
            BufferedReader reader = request.getReader();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }

            String postData = sb.toString();
            if (StrUtil.isNotEmpty(postData)) {
                JSONObject json = JSONObject.parseObject(postData);
                paramsMap.putAll(json);
            }
        }

        return paramsMap;
    }

    /**
     * 转换成接口返回参数.
     *
     * @param returnParams 接口返回参数定义
     * @param item         数据
     * @return 转换后的返回数据
     */
    private Map<String, Object> getResultParams(List<ApiParams> returnParams, Map<String, Object> item) {
        Map<String, Object> resultItem = new HashMap<>();
        if (returnParams != null && !returnParams.isEmpty() && item != null) {
            for (ApiParams apiParam : returnParams) {
                resultItem.put(apiParam.getName(), item.get(apiParam.getField()));
            }
        }

        return resultItem;
    }

    /**
     * 转换成接口返回参数.
     *
     * @param returnParams 接口返回参数定义
     * @param items        数据集
     * @return 转换后的返回数据
     */
    private List<Map<String, Object>> getResultParamsList(List<ApiParams> returnParams, List<Map<String, Object>> items) {
        List<Map<String, Object>> resultItems = new ArrayList<>();
        if (null != items && !items.isEmpty()) {
            for (Map<String, Object> item : items) {
                resultItems.add(getResultParams(returnParams, item));
            }
        }

        return resultItems;
    }

    /**
     * 获取分页的参数.
     * @param request 请求对象
     * @return 分页参数
     */
    private Page<?> getPage(HttpServletRequest request) {
        Page<?> page = new Page<>();
        String pageSize = request.getParameter("pageSize");
        String pageNum = request.getParameter("pageNum");
        if (StrUtil.isNotEmpty(pageSize)) {
            page.setPageSize(Integer.parseInt(pageSize));
        } else {
            page.setPageSize(10);
        }

        if (StrUtil.isNotEmpty(pageNum)) {
            page.setPageNum(Integer.parseInt(pageNum));
        } else {
            page.pageNum(1);
        }

        return page;
    }

    /**
     * 执行动态SQL.
     * @param api 接口定义
     * @param parameters 参数
     * @param pageable 是否分页
     * @param page 分页参数
     * @return 执行结果
     */
    public Object executeDynamicScript(ApiVo api, Map<String, Object> parameters, boolean pageable, Page<?> page) {
        Object result = null;
        String dynamicSql = "<script>" + api.getScript() + "</script>";

        try (SqlSession sqlSession = sessionFactory.openSession())  {
            Configuration configuration = sqlSession.getConfiguration();

            SqlCommandType sqlCommandType = SqlCommandType.UNKNOWN;
            String sqlScript = api.getScript().trim().toLowerCase();
            if (sqlScript.startsWith("select")) {
                sqlCommandType = SqlCommandType.SELECT;
            } else if (sqlScript.startsWith("insert")) {
                sqlCommandType = SqlCommandType.INSERT;
            } else if (sqlScript.startsWith("update")) {
                sqlCommandType = SqlCommandType.UPDATE;
            } else if (sqlScript.startsWith("delete")) {
                sqlCommandType = SqlCommandType.DELETE;
            }

            // 创建 MappedStatement
            String statementId = "dynamicStatement-" + api.getId() + "-" + System.currentTimeMillis();

            try {

                // 使用 XMLLanguageDriver 创建 SqlSource
                XMLLanguageDriver languageDriver = new XMLLanguageDriver();
                SqlSource sqlSource = languageDriver.createSqlSource(configuration, dynamicSql, Map.class);

                if (!configuration.hasStatement(statementId)) {
                    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, statementId, sqlSource, sqlCommandType);
                    statementBuilder.resultMaps(Collections.singletonList(new ResultMap.Builder(
                            configuration, "defaultResultMap", Map.class, Collections.emptyList()).build()));
                    configuration.addMappedStatement(statementBuilder.build());
                }

                // 执行 SQL
                switch (sqlCommandType) {
                    case SELECT:
                        if (api.getReturnType().equals(ReturnType.OBJECT.getVal())) {
                            // 查询, 返回单个对象
                            Map<String, Object> results = sqlSession.selectOne(statementId, parameters);
                            result = getResultParams(api.getReturnParams(), results);
                        } else if (api.getReturnType().equals(ReturnType.ARRAY.getVal())) {
                            RowBounds rowBounds = null;
                            if (pageable) {
                                // 分页
                                rowBounds = new RowBounds((page.getPageNum() - 1) * page.getPageSize(), page.getPageSize());
                            } else {
                                // 不分页, 最多返回1000条
                                rowBounds = new RowBounds(0, 1000);
                            }
                            List<Map<String, Object>> results = sqlSession.selectList(statementId, parameters, rowBounds);
                            List<Map<String, Object>> data = getResultParamsList(api.getReturnParams(), results);
                            if (pageable) {
                                // 分页返回分页对象
//                                PageInfo<?> pageInfo = new PageInfo<>(data);
//                                pageInfo.setPageNum(page.getPageNum());
//                                pageInfo.setPageSize(page.getPageSize());
//                                pageInfo.setTotal(total(api, parameters));
//                                pageInfo.setPages((int) Math.ceil((double) pageInfo.getTotal() / pageInfo.getPageSize()));
                                int total = total(api, parameters);
                                return PageUtil.pageInfo(page.getPageNum(), page.getPageSize(), total, data);
                            } else {
                                return data;
                            }
                        }

                        break;
                    case INSERT:
                        int insertCount = sqlSession.insert(statementId, parameters);
                        log.info("Inserted rows: {}", insertCount);
                        break;
                    case UPDATE:
                        int updateCount = sqlSession.update(statementId, parameters);
                        log.info("Updated rows: {}", updateCount);
                        break;
                    case DELETE:
                        int deleteCount = sqlSession.delete(statementId, parameters);
                        log.info("Deleted rows: {}", deleteCount);
                        break;
                    default:
                        throw new IllegalArgumentException("Unsupported SqlCommandType: " + sqlCommandType);
                }
            } finally {
                // 注销 MappedStatement
                if (configuration.hasStatement(statementId)) {
                    configuration.getMappedStatementNames().remove(statementId);
                }
            }

            return result;
        } finally {

        }
    }

    /**
     * 查询分页的总记录数.
     * @param api API定义
     * @param params 参数
     * @return 总记录数
     */
    private int total(ApiVo api, Map<String, Object> params) {
        String countSql = "<script>SELECT COUNT(*) FROM (" + api.getScript() + ") AS tmp</script>";

        try (SqlSession sqlSession = sessionFactory.openSession()) {
            Configuration configuration = sqlSession.getConfiguration();

            String statementId = "dynamicStatement-count-" + api.getId() + "-" + System.currentTimeMillis();

            try {
                // 使用 XMLLanguageDriver 创建 SqlSource
                XMLLanguageDriver languageDriver = new XMLLanguageDriver();
                SqlSource sqlSource = languageDriver.createSqlSource(configuration, countSql, Map.class);

                if (!configuration.hasStatement(statementId)) {
                    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(
                            configuration, statementId, sqlSource, SqlCommandType.SELECT);
                    statementBuilder.resultMaps(Collections.singletonList(new ResultMap.Builder(configuration, "defaultResultMap", Integer.class, Collections.emptyList()).build()));
                    configuration.addMappedStatement(statementBuilder.build());
                }

                Object object = sqlSession.selectOne(statementId, params);

                return (int) object;
            } finally {
                // 注销 MappedStatement
                if (configuration.hasStatement(statementId)) {
                    configuration.getMappedStatementNames().remove(statementId);
                }
            }
        }
    }
}
